Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BAAS-28253: Add optional tick tracking at the function level #115

Merged
merged 18 commits into from
Feb 12, 2024

Conversation

Calvinnix
Copy link

No description provided.

vm.go Outdated Show resolved Hide resolved
compiler_test.go Outdated Show resolved Hide resolved
runtime.go Outdated Show resolved Hide resolved
vm.go Outdated Show resolved Hide resolved
vm.go Outdated Show resolved Hide resolved
@Calvinnix Calvinnix marked this pull request as ready for review January 31, 2024 23:42
@Calvinnix
Copy link
Author

The performance impact of this change doesn't look too bad. (benchmark-vm)

runtime.go Outdated Show resolved Hide resolved
Copy link

@arahmanan arahmanan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sill need to look through all files, but posting some minor comments for now

runtime.go Outdated Show resolved Hide resolved
runtime.go Outdated Show resolved Hide resolved
runtime.go Outdated Show resolved Hide resolved
Copy link

@LijieZhang1998 LijieZhang1998 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea to track functions. I have some questions about implementation.

runtime.go Outdated
@@ -1513,7 +1522,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
vm.stash = &r.global.stash
vm.sb = vm.sp - 1
}
vm.prg = p
vm.setPrg(p)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] We calculate the ticks before vm.runTry() or vm.run()where ticks is ticking(vm.r.ticks++). Would it return 0 in some case? Correct me. I may be lost somewhere.
[q] can we add some tests to see if we're able to get the ticks metrics correctly?e.g. creating a vm to run a function and return this metrics. It may be not easy because we need to know the expected ticks(lol).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some background I had a previous iteration of this that tracked ticks at the exec call but that came with a ~20-30% performance hit.

		} else {
                         // tick tracking occurred here
			vm.prg.code[vm.pc].exec(vm)
		}

[q] We calculate the ticks before vm.runTry() or vm.run()where ticks is ticking(vm.r.ticks++). Would it return 0 in some case? Correct me. I may be lost somewhere.

Yes there may be some scenarios on initial setup where this gets hit and no new ticks have been calculated yet. I think for the run and runTry scenario you are referencing it would probably get handled by the existing vm.prg being nil (i.e. this check in setPrg if vm.prg != nil {)

I like your idea to add tests, I originally compared the setPrg solution against the results of the verbose solution I referenced above and did not see any discrepancies, but I'll add a few tests to solidify that check.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q1] yes, I believe in some cases the ticks could be 0, but I believe that's fine. I'll let Calvin confirm that thought :)

[q2] agreed. Now that we're closer to a solution we should start adding some tests. We don't need to check exact ticks, but we should make sure that the metrics contain the correct functions and that the ticks of an expensive JS function are higher than the ticks of trivial functions. It might also be worth testing that executing a function X times generates X times more ticks than executing the same function once.

vm.go Outdated Show resolved Hide resolved
vm.go Outdated Show resolved Hide resolved
runtime.go Outdated
@@ -1513,7 +1522,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
vm.stash = &r.global.stash
vm.sb = vm.sp - 1
}
vm.prg = p
vm.setPrg(p)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q1] yes, I believe in some cases the ticks could be 0, but I believe that's fine. I'll let Calvin confirm that thought :)

[q2] agreed. Now that we're closer to a solution we should start adding some tests. We don't need to check exact ticks, but we should make sure that the metrics contain the correct functions and that the ticks of an expensive JS function are higher than the ticks of trivial functions. It might also be worth testing that executing a function X times generates X times more ticks than executing the same function once.

runtime.go Outdated Show resolved Hide resolved
vm.go Outdated
func (vm *vm) setPrg(prg *Program) {
if vm.r.functionTickTrackingEnabled {
currentTicks := vm.r.Ticks()
if vm.prg != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean by the situation of returning 0 is when vm.prg != nil. In an extreme case, in the very beginning, vm.r.Ticks() should return 0 because ticks is not ticking yet. vm.lastFunctionTicks is also 0. So line 328 will plus 0 at that time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that edge case is ok.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I say that because I think I prefer the noop (adding 0) over the logic it would take to avoid that scenario.

Let me know if you disagree though!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this is fine, but @LijieZhang1998 let me know if you disagree/if I'm misunderstanding your comment. We don't care too much about these metrics being 100% accurate. We just want to figure out which are the top 10 or so functions we should be focusing on improving.

@@ -489,6 +498,8 @@ func (r *Runtime) init() {
}
r.vm.init()

r.tickMetrics = make(map[string]uint64)
Copy link
Author

@Calvinnix Calvinnix Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to make this map get initialized per the functionTickTrackingEnabled setting but this actual would cause a problem (as demonstrated by the test). We need to have the vm object in order to enable the tick tracking but we also check if it is enabled when we create the vm object. So I think its better to just always initialize this map.

			vm := New()
			if tc.functionTickTrackingEnabled {
				vm.EnableFunctionTickTracking()
			}

(^ snippet from the unit test)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[opt] I would document this in code

runtime.go Outdated
@@ -3229,3 +3243,11 @@ func (r *Runtime) getPrototypeFromCtor(newTarget, defCtor, defProto *Object) *Ob
}
return defProto
}

func (self *Runtime) EnableFunctionTickTracking() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[opt] similar comment here about dropping the word "Function". Also, thoughts on grouping all the new tick functions together? i.e. move the TickMetrics down here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the only reason I left "Function" here is "EnableTickTracking" sounds like it is for the main tick counter i.e. vm.r.ticks++... However we could do something like "EnableTickMetricTracking"

I agree on grouping these functions together. I'll do that.

vm.go Outdated
func (vm *vm) setPrg(prg *Program) {
if vm.r.functionTickTrackingEnabled {
currentTicks := vm.r.Ticks()
if vm.prg != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this is fine, but @LijieZhang1998 let me know if you disagree/if I'm misunderstanding your comment. We don't care too much about these metrics being 100% accurate. We just want to figure out which are the top 10 or so functions we should be focusing on improving.

vm.go Outdated
if vm.prg != nil {
function := string(vm.prg.funcName)
if vm.prg.src != nil {
function = vm.prg.src.Name() + "_" + function

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[mega-nit] function names could have an underscore. Thoughts on going with a period instead?

Suggested change
function = vm.prg.src.Name() + "_" + function
function = vm.prg.src.Name() + "." + function

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I went with a _ is because the prg.src.Name() can be a file name so test.js.functionName looked a bit weird. I can go back to the period though, I don't have a strong opinion on this.

compiler_test.go Outdated
@@ -156,6 +156,7 @@ func testLibX() *Program {

func (r *Runtime) testPrg(p *Program, expectedResult Value, t *testing.T) {
vm := r.vm
vm.profileTicks()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I'll remove, this is leftover from changing all of the vm.prg sets to use the setPrg function.

func.go Outdated
@@ -227,6 +227,7 @@ func (f *classFuncObject) _initFields(instance *Object) {
if f.initFields != nil {
vm := f.val.runtime.vm
vm.pushCtx()
vm.profileTicks()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] could we call vm.profileTicks() within vm.pushCtx()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a great idea! This handles a majority of the scenarios. There was a scenario that I left the vm.profileTicks() call but I'll comment on it after I push.

vm.go Outdated
}

type instruction interface {
exec(*vm)
}

// profileTicks tracks the ticks for the current Program, this should be called prior to updating the program (i.e. vm.prg = p)
func (vm *vm) profileTicks() {
if vm.r.functionTickTrackingEnabled {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] let's flip this check and return early instead. That'll reduce indentation and improve the readability of this function. i.e.

if !vm.r.functionTickTrackingEnabled {
  return
}

vm_test.go Outdated
f()
`,
functionTickTrackingEnabled: true,
expectedTickMetrics: map[string]uint64{"test.js_": 6, "test.js_f": 809},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests will break fairly easily if we compare the exact ticks. We don't really care about the tick values. We just want to make sure that the values are reasonable when compared to one another.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just check to make sure that the keys line up and the values are greater than 0

},
}

for _, tc := range tests {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q1]can we add some more tests such as executing executing class functions and multiple nested functions?

[q2] Also, do you mind sharing the metrics that we would generate from baas when using a dependency such as the aws sdk? The easiest way to do this might be to run our dependencies tests in evergreen with the tracking enabled.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q1]can we add some more tests such as executing executing class functions and multiple nested functions?

Yeah absolutely!

[q2] Also, do you mind sharing the metrics that we would generate from baas when using a dependency such as the aws sdk? The easiest way to do this might be to run our dependencies tests in evergreen with the tracking enabled.

Would that data be in splunk? Or would we need to parse the output of the tests?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evergreen task with tick metric tracking hardcoded to true. Will update this thread with the results.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'm curious to see if the current approach makes it easy enough for us to identify which function is consuming the most amount of this. Let me know when this is ready.

Copy link
Author

@Calvinnix Calvinnix Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm for some reason the logs aren't showing the "tick metrics" logs for that evergreen run.

I think my local testing might alleviate some of your concerns though.

exports = async function(arg){
  const { Buffer } = require('buffer');
  const loremIpsumText = // long string
  return Buffer.from(loremIpsumText);
};

image
[Note that the tickMetricsToLog array is sorted so we are looking at the top 5 tick contributors]

The majority of usage for this Buffer.from example is BufferJS::utf8ToBytes and BufferJS::blitBuffer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also callout that these tick metrics are flat (not cumulative) in that we are measuring the specific time only for that function.

So in this case we would know that we would want to have a native implementation in Go for utf8ToBytes and probably blitBuffer as well, which is nice because this allows us to not have to potentially waste time porting over the entire Buffer.from implementation.

Copy link
Author

@Calvinnix Calvinnix Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm for some reason the logs aren't showing the "tick metrics" logs for that evergreen run.

I have a feeling this is happening because of how tick metrics are aggregated and published asynchronously. It doesn't even look like the cmd/server/main.go executes which is where this asynchronous logic would start.

I went ahead and set this up manually though, here are the results when running against aws-sdk-v3/s3

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmac lol

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice work, hello mr Buffer not surprised to see you up there!

@@ -1528,6 +1533,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
vm.clearStack()
} else {
vm.stack = nil
vm.profileTicks()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this we would miss the ticks tracked for the above vm.runTry

	ex := vm.runTry(r.vm.ctx)
	if ex == nil {
		result = r.vm.result
	} else {
		err = ex
	}
	if recursive {
		vm.popCtx()
		vm.halt = false
		vm.clearStack()
	} else {
		vm.stack = nil
		vm.profileTicks()
		vm.prg = nil
		vm.setFuncName("")
		r.leave()
	}

It could turn out that this isn't necessary because it is captured by the pushCtx/restoreCtx, but I don't think it hurts to have this in place just to make sure we capturing any lingering ticks for the program. Worst case scenario is there are no new ticks and we add 0 for the program.

If we think this clutters the code I can dig more into this and see if it is necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah without this we would miss a few ticks in a few edge cases, not the end of the world but probably nice to keep around.

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it works for me.

@@ -1528,6 +1533,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
vm.clearStack()
} else {
vm.stack = nil
vm.profileTicks()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it works for me.

runtime.go Outdated
Comment on lines 3239 to 3245
func (self *Runtime) EnableTickMetricTracking() {
self.tickMetricTrackingEnabled = true
}

func (self *Runtime) DisableTickMetricTracking() {
self.tickMetricTrackingEnabled = false
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a comment for these exported functions?

vm_test.go Outdated
f()
`,
tickMetricTrackingEnabled: true,
expectedTickMetricsKeys: []string{"test.js.", "test.js.f"},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[opt] Continuing this thread here.
ah good point... Yeah I don't have a strong opinion either. Maybe we can go with ::?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo I really like :: thanks!!

},
}

for _, tc := range tests {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'm curious to see if the current approach makes it easy enough for us to identify which function is consuming the most amount of this. Let me know when this is ready.

Copy link

@LijieZhang1998 LijieZhang1998 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 💯

@arahmanan
Copy link

The performance impact of this change doesn't look too bad. (benchmark-vm)

@Calvinnix just to confirm, these benchmarks are with the ticks profiling enabled, right?

Copy link

@arahmanan arahmanan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! pending this question

@Calvinnix
Copy link
Author

The performance impact of this change doesn't look too bad. (benchmark-vm)

@Calvinnix just to confirm, these benchmarks are with the ticks profiling enabled, right?

That is correct, that benchmark was from when the tick profiling was enabled by default and nothing material has changed with the logic since then.

Copy link

@Gabri3l Gabri3l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work here, I just wanted one more test case and we're good to go!

@@ -489,6 +498,8 @@ func (r *Runtime) init() {
}
r.vm.init()

r.tickMetrics = make(map[string]uint64)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[opt] I would document this in code

@@ -594,3 +594,130 @@ func TestStashMemUsage(t *testing.T) {
})
}
}

func TestTickTracking(t *testing.T) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The one test I'm missing here is a function that throws an error. I was playing around with popCtx vs restoreCtx and most of the times profiling from either yields the same results. The only exception was when I was using it inside of the restoreCtx (like you are in this PR) where I noticed different ticks values when my function was throwing an error. Just thought it would be useful to have that case here. Realistically it will pass the test anyway and we won't see much different but profiling from popCtx only was missing out on some ticks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I added a test to cover that we track ticks for a function that errors. 👍

@Calvinnix Calvinnix merged commit acea50a into mongodb-forks:realm Feb 12, 2024
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants